package com.nado.extensions.data;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.nado.extensions.util.ResourceLoader;
import com.nado.extensions.util.XMLDOMUtil;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;

/**
 * XML 타입의 환경 정보 데이터 모델에 대한 {@link DataHandler} 구현체.
 * <p/>
 * 하위 노드에 대한 검색은 XPath API를 이용한다.
 *
 */
public class JaxpXmlDataHandler extends AbstractDataHandler {

	/**
	 * 최상위 {@link Element}
	 */
	private Node element;

	/**
	 * 설정된 위치의 파일을 이용하여 XMLDataHandler를 생성한다.
	 */
	JaxpXmlDataHandler(String location) {
		super(location, true);
		initialize();
	}

	/**
	 * {@link Element} 파라미터를 통해 XMLDataHandler를 생성한다.
	 */
	JaxpXmlDataHandler(Node element) {
		this.element = element;
	}

	public void initialize() {
		ResourceLoader loader = new ResourceLoader();
		Element localElement = parseResource(location, loader);
		includeSubConfig(loader, localElement, localElement);
		this.element = localElement.cloneNode(true);
	}

	public void destroy() {
		if (element.getOwnerDocument() == null) {
			XMLDOMUtil.destroyDocument((Document) element);
		}
	}

	/**
	 * 파일을 읽은 후 최상위 {@link Element}를 반환한다.
	 *
	 * @param location 환경 파일의 위치
	 * @param loader   클래스 경로의 하위 자원을 검색하기 위한 {@link ClassLoader}
	 */
	private Element parseResource(String location, ResourceLoader loader) {
		InputStream inputStream;
		try {
			inputStream = getInputStream(location, loader);
		} catch (Exception e) {
			throw new DataHandlerException(e);
		}
		return parseResource(inputStream);
	}

	private Element parseResource(InputStream inputStream) {
		try {
			return XMLDOMUtil.loading(inputStream).getDocumentElement();
		} catch (Exception e) {
			throw new DataHandlerException(e);
		}
	}

	/**
	 * 환경 정보의 include 속성으로 포함된 환경 정보를 읽어들여 최상위 노드에 추가한다.
	 *
	 * @param loader	  클래스 경로 하위 자원을 검색하기 위한 {@link ClassLoader}
	 * @param rootElement 최상위 {@link Element}
	 * @param element	 include 요소를 포함하고 있는 {@link Element}
	 */
	private void includeSubConfig(ResourceLoader loader, Element rootElement,
								  Element element) {
		NodeList includes = ((Element) element.cloneNode(true))
				.getElementsByTagName("include");

		for (int idx = 0; idx < includes.getLength(); idx++) {
			// base config 파일 내의 include config 추출
			Element includeInfo = (Element) includes.item(idx);
			Element included = parseResource(includeInfo
					.getFirstChild().getNodeValue(), loader);

			// inclueded config의 내용을 base config 에 붙임
			NodeList childNodes = included.getChildNodes();
			for (int subIdx = 0; subIdx < childNodes.getLength(); subIdx++) {
				Node orgChildNode = childNodes.item(subIdx);
				Node newChildNode = rootElement.getOwnerDocument().importNode(
						orgChildNode, true);
				rootElement.appendChild(newChildNode);
			}

			log.info("include config "
					+ includeInfo.getFirstChild().getNodeValue());

			// recursively include sub-config
			includeSubConfig(loader, rootElement, included);
		}
	}

	public String get(String key) {
		Node selected = XMLDOMUtil.getNode(element, key);
		if (selected == null) {
			throw new DataHandlerException(key + " not found in config");
		}

		String result = XMLDOMUtil.evaluate(selected);

		Matcher matcher = pattern.matcher(result);

		StringBuffer sb = new StringBuffer();
		while (matcher.find()) {
			matcher.appendReplacement(sb, get(matcher.group().substring(2,
					matcher.group().length() - 1)));
		}
		matcher.appendTail(sb);
		return sb.toString();
	}

	public boolean containsKey(String key) {
		Node selected = XMLDOMUtil.getNode(element, key);
		return selected != null;
	}

	public List<String> getValues(String key) {
		NodeList elements = XMLDOMUtil.getNodes(element, key);
		if (elements == null) {
			return Collections.emptyList();
		}
		List<String> values = new ArrayList<String>(elements.getLength());
		for (int idx = 0; idx < elements.getLength(); idx++) {
			values.add(XMLDOMUtil.evaluate(elements.item(idx)));
		}
		return values;
	}

	public DataHandler peek(String path) {
		Node subConfigElement = XMLDOMUtil.getNode(element, path);
		if (subConfigElement == null) {
			return null;
		}
		return new JaxpXmlDataHandler(subConfigElement);
	}

	public DataHandler select(String path) throws DataHandlerException {
		DataHandler handler = peek(path);
		if (handler == null) {
			throw new DataHandlerException(path + " not found in config");
		}
		return handler;
	}

	public List<DataHandler> selectAll(String path) {
		List<DataHandler> subConfigs = peekAll(path);
		if (subConfigs.isEmpty()) {
			throw new DataHandlerException(path + " not found in config");
		}
		return subConfigs;
	}

	public List<DataHandler> peekAll(String path) {
		NodeList nodeList = XMLDOMUtil.getNodes(element, path);
		if (nodeList == null) {
			return Collections.emptyList();
		}
		List<DataHandler> subConfigs = new ArrayList<DataHandler>(nodeList.getLength());
		for (int idx = 0; idx < nodeList.getLength(); idx++) {
			subConfigs.add(new JaxpXmlDataHandler(nodeList.item(idx)));
		}
		return subConfigs;
	}

	public List<DataHandler> peekChildren() {
		NodeList nodeList = element.getChildNodes();
		List<DataHandler> subConfigs = new ArrayList<DataHandler>(nodeList.getLength());
		for (int idx = 0; idx < nodeList.getLength(); idx++) {
			Node node = nodeList.item(idx);
			if (node.getNodeType() == Node.ELEMENT_NODE) {
				subConfigs.add(new JaxpXmlDataHandler(node));
			}
		}
		return subConfigs;
	}

	public String getRootName() {
		return element.getNodeName();
	}
}
